# -*- coding: utf-8 -*-

# Standard imports.
from datetime import datetime
from copy import deepcopy

# Trac imports.
from trac.core import Interface, Component, implements, ExtensionPoint, TracError
from trac.config import Option, IntOption, ListOption
from trac.mimeview import Context
from trac.perm import PermissionError
from trac.web.chrome import add_link, add_stylesheet, add_script, add_notice
from trac.web.href import Href
from trac.resource import Resource
from trac.wiki.formatter import format_to_oneliner
from trac.attachment import AttachmentModule
from trac.util.datefmt import to_timestamp, utc
from trac.util.text import to_unicode
from trac.util.presentation import Paginator
from trac.db.api import with_transaction

# Trac interfaces
from trac.resource import IResourceManager
from trac.perm import IPermissionRequestor
from trac.attachment import ILegacyAttachmentPolicyDelegate

# Genshi imports.
from genshi.input import HTML
from genshi.core import Markup
from genshi.filters import Transformer

# Discussion plugin internals
from tracdiscussion.listeners import IForumChangeListener, ITopicChangeListener, IMessageChangeListener

# Multiproject imports
from multiproject.common.projects import Projects
from multiproject.core.util import resolve_project_id
from multiproject.core.users import get_userstore
from multiproject.core.configuration import Configuration
from multiproject.core.db import safe_int

conf = Configuration.instance()

class IDiscussionFilter(Interface):
    """Extension point interface for components that want to filter discussion
    topics and messages before their addition."""

    def filter_topic(context, topic):
        """ Called before new topic creation. May return tuple (False,
        <error_message>) or (True, <topic>) where <error_message> is a message
        that will be displayed when topic creation will be canceled and <topic>
        is modified topic that will be added."""

    def filter_message(context, message):
        """ Called before new message creation. May return tuple (False,
        <error_message>) or (True, <message>) where <error_message> is a
        message that will be displayed when message creation will be canceled
        and <message> is modified message that will be added."""

class DiscussionApi(Component):
    """
        The API component implements most of functionality of the plugin,
        especially database access and request handling.
    """
    implements(IResourceManager, ILegacyAttachmentPolicyDelegate,
      IPermissionRequestor)

    # Configuration options.
    default_topic_display = Option('discussion', 'default_topic_display',
                                   'classic', 'Default display mode for forum topics list.')
    default_message_display = Option('discussion', 'default_message_display',
                                     'tree', 'Default display mode for topic messages list.')
    forum_sort = Option('discussion', 'forum_sort', 'lasttopic', 'Column by which' +
                        ' will be sorted forum lists. Possible values are: id group name'
                        ' subject time moderators description topics replies lasttopic lastreply')
    forum_sort_direction = Option('discussion', 'forum_sort_direction', 'asc',
                                  'Direction of forum lists sorting. Possible values are: asc desc.')
    topic_sort = Option('discussion', 'topic_sort', 'lastreply', 'Column by which' +
                        ' will be sorted topic lists. Possible values are: id forum subject' +
                        ' time author body replies lastreply.')
    topic_sort_direction = Option('discussion', 'topic_sort_direction', 'asc',
                                  'Direction of topic lists sorting. Possible values are: asc desc.')
    topics_per_page = IntOption('discussion', 'topics_per_page', 30,
                                'Number of topics per page in topic list.')
    messages_per_page = IntOption('discussion', 'messages_per_page', 50,
                                  'Number of messages per page in message list.')

    additional_options = ListOption('discussion', 'additional_options', default = '',
                                    doc = 'additional options to upgrade environment')
    use_default_forum = False

    visual_status_tag_important = Option('discussion', 'visual_status_tag_important',
                                         '[STICKY]', 'String shown for important topics')
    visual_status_tag_solved = Option('discussion', 'visual_status_tag_solved',
                                      '[SOLVED]', 'String shown for solved topics')
    visual_status_tag_unsolved = Option('discussion', 'visual_status_tag_unsolved',
                                        '', 'String shown for unsolved topics')
    visual_status_tag_locked = Option('discussion', 'visual_status_tag_locked',
                                      '[LOCKED]', 'String shown for locked topics')

    # Valid table and column names
    VALID_TABLES = ('message', 'topic', 'forum', 'forum_group')
    VALID_COLUMNS = ('moderators', 'subscribers', 'tags', 'status', 'description',
                     'author', 'forum_group', 'time', 'subject', 'name', 'body',
                     'forum', 'priority', 'replyto', 'topic', 'id')

    def __init__(self):
        for option in self.additional_options:
            if str(option) == 'use_default_forum':
                self.use_default_forum = True

    # Extension points.
    forum_change_listeners = ExtensionPoint(IForumChangeListener)
    topic_change_listeners = ExtensionPoint(ITopicChangeListener)
    message_change_listeners = ExtensionPoint(IMessageChangeListener)
    discussion_filters = ExtensionPoint(IDiscussionFilter)

    # IPermissionRequestor methods.
    def get_permission_actions(self):
        view = 'DISCUSSION_VIEW'
        append = ('DISCUSSION_APPEND', ['DISCUSSION_VIEW'])
        attach = ('DISCUSSION_ATTACH', ['DISCUSSION_APPEND', 'DISCUSSION_VIEW'])
        moderate = ('DISCUSSION_MODERATE', ['DISCUSSION_VIEW',
                                            'DISCUSSION_ATTACH'])
        admin = ('DISCUSSION_ADMIN', ['DISCUSSION_VIEW', 'DISCUSSION_APPEND',
                                      'DISCUSSION_ATTACH', 'DISCUSSION_MODERATE'])

        news_create = 'DISCUSSION_ANNOUNCECREATE'
        news_append = 'DISCUSSION_ANNOUNCEAPPEND'
        return [view, append, attach, moderate, news_create, news_append, admin]

    # ILegacyAttachmentPolicyDelegate methods.

    def check_attachment_permission(self, action, username, resource, perm):
        if resource.parent.realm == 'discussion':
            if action in ['ATTACHMENT_VIEW', 'ATTACHMENT_CREATE', \
                          'ATTACHMENT_DELETE']:
                return 'DISCUSSION_ATTACH' in perm(resource.parent)

    # IResourceManager methods.

    def get_resource_realms(self):
        yield 'discussion'

    def get_resource_url(self, resource, href, **kwargs):
        if resource.id:
            return href(resource.realm, resource.id, **kwargs)
        else:
            return href(resource.realm, **kwargs)

    def get_resource_description(self, resource, format = None, **kwargs):
        # Create context.
        context = Context('discussion-core')

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        type, id = resource.id.split('/')

        # Generate description for forums.
        if type == 'forum':
            forum = self._get_item(context, 'forum', ('id', 'name', 'subject'),
                                   where='id', values=(id,))
            if format == 'compact':
                return '#%s' % (forum['id'],)
            elif format == 'summary':
                return 'Forum %s - %s' % (forum['name'], forum['subject'])
            else:
                return 'Forum %s' % (forum['name'],)

        # Generate description for topics.
        elif type == 'topic':
            topic = self._get_item(context, 'topic', ('id', 'subject'),
                                   where='id', values=(id,))
            if format == 'compact':
                return '#%s' % (topic['id'],)
            elif format == 'summary':
                return 'Topic #%s (%s)' % (topic['id'], topic['subject'])
            else:
                return 'Topic #%s' % (topic['id'],)

        # Generate description for messages.
        elif type == 'message':
            if format == 'compact':
                return '#%s' % (id,)
            elif format == 'summary':
                return 'Message #%s' % (id,)
            else:
                return 'Message #%s' % (id,)


    def resource_exists(self, resource):
        # Create context.
        context = Context('discussion-core')

        # Get database access.
        db = self.env.get_db_cnx()
        context.cursor = db.cursor()

        type, id = resource.id.split('/')

        # Check if forum exists.
        if type == 'forum':
            return self._get_item(context, 'forum', ('id',), where='id',
                                  values=(id,)) != None

        # Check if topic exits.
        elif type == 'topic':
            return self._get_item(context, 'topic', ('id',), where='id',
                                  values=(id,)) != None

        # Check if message exists.
        elif type == 'message':
            return self._get_item(context, 'message', ('id',), where='id',
                                  values=(id,)) != None


    # Main request processing function.

    def process_discussion(self, context):
        actions = []

        # Get database connection
        @with_transaction(self.env)
        def implementation(db):
            # Note that the cursor is closed after the implementation function ends
            context.cursor = db.cursor()

            # Get request items and actions.
            self._prepare_context(context)
            actions.extend(self._get_actions(context))
            self.log.debug('actions: %s' % (actions,))
            self.log.debug('req.args: %s' % (str(context.req.args)))

            # Get session data.
            context.visited_forums = eval(context.req.session.get('visited-forums') or '{}')
            context.visited_topics = eval(context.req.session.get('visited-topics') or '{}')

            # Perform actions.
            self._do_actions(context, actions)

        # Update session data.
        context.req.session['visited-topics'] = to_unicode(context.visited_topics)
        context.req.session['visited-forums'] = to_unicode(context.visited_forums)

        # Fill up template data structure.
        context.data['users'] = context.users
        context.data['has_tags'] = context.has_tags
        context.data['group'] = context.group
        context.data['forum'] = context.forum
        context.data['topic'] = context.topic
        context.data['message'] = context.message
        context.data['moderator'] = context.moderator
        context.data['authname'] = context.req.authname
        context.data['authemail'] = context.authemail
        context.data['realm'] = context.realm
        context.data['mode'] = actions[-1]
        context.data['time'] = datetime.now(utc)
        context.data['env'] = self.env
        context.data['discussion_url'] = context.req.href.discussion()

        # Add CSS styles and scripts.
        add_stylesheet(context.req, 'common/css/wiki.css')
        add_stylesheet(context.req, 'discussion/css/discussion.css')
        add_stylesheet(context.req, 'discussion/css/admin.css')
        add_script(context.req, 'common/js/trac.js')
        add_script(context.req, 'common/js/search.js')
        add_script(context.req, 'common/js/wikitoolbar.js')
        add_script(context.req, 'discussion/js/discussion.js')

        # Determine template name.
        context.template = self._get_template(context, actions)

        # Return request template and data.
        self.log.debug('template: %s data: %s' % (context.template, context.data,))
        return context.template, {'discussion' : context.data}

    # Internal methods.

    def _prepare_context(self, context):
        # Prepare template data.
        context.data = {}

        # Get list of Trac users.
        context.users = self.get_users(context)

        # Check if TracTags plugin is enabled.
        context.has_tags = self.env.is_component_enabled('tracdiscussion.tags.DiscussionTags')

        # Populate active message.
        context.group = None
        context.forum = None
        context.topic = None
        context.message = None
        if context.req.args.has_key('message'):
            message_id = int(context.req.args.get('message') or 0)
            context.message = self.get_message(context, message_id)
            if context.message:
                context.topic = self.get_topic(context, context.message['topic'])
                context.forum = self.get_forum(context, context.topic['forum'])
                context.group = self.get_group(context, context.forum['forum_group'])

                # Create request resource.
                context.resource = Resource('discussion', 'message/%s' % (context.message['id'],))
            else:
                raise TracError('Message with ID %s does not exist.' % (message_id,))

        # Populate active topic.
        elif context.req.args.has_key('topic'):
            topic_id = int(context.req.args.get('topic') or 0)
            context.topic = self.get_topic(context, topic_id)
            if context.topic:
                context.forum = self.get_forum(context, context.topic['forum'])
                context.group = self.get_group(context, context.forum['forum_group'])

                # Create request resource.
                context.resource = Resource('discussion', 'topic/%s' % (context.topic['id'],))
            else:
                raise TracError('Topic with ID %s does not exist.' % (topic_id,))

        # Populate active forum.
        elif context.req.args.has_key('forum'):
            forum_id = int(context.req.args.get('forum') or 0)
            context.forum = self.get_forum(context, forum_id)
            if context.forum:
                context.group = self.get_group(context, context.forum['forum_group'])

                # Create request resource.
                context.resource = Resource('discussion', 'forum/%s' % (context.forum['id'],))
            else:
                raise TracError('Forum with ID %s does not exist.' % (forum_id,))

        # Populate active group.
        elif context.req.args.has_key('group'):
            group_id = int(context.req.args.get('group') or 0)
            context.group = self.get_group(context, group_id)

            # Create request resource.
            context.resource = Resource('discussion', 'group/%s' % (context.group['id'],))

            if not context.group:
                raise TracError('Group with ID %s does not exist.' % (group_id,))

        # Determine moderator rights.
        context.moderator = (context.forum and (context.req.authname in context.forum['moderators']) and
                             context.req.perm.has_permission('DISCUSSION_MODERATE') or
                             context.req.perm.has_permission('DISCUSSION_ADMIN'))

        # Determine if current forum is news forum
        if context.forum and context.forum['name'] == conf.news_forum_name:
            context.is_news_forum = True
        else:
            context.is_news_forum = False

        # Determine news creating rights.
        context.perm_create_news = context.req.perm.has_permission('DISCUSSION_ANNOUNCECREATE')

        # Determine news appending rights.
        context.perm_append_news = context.req.perm.has_permission('DISCUSSION_ANNOUNCEAPPEND')

        # Determine if user is subscriber to topic.
        context.authemail = context.req.session.get('email')
        context.forum_subscriber = context.forum and (context.authemail in context.forum['subscribers'])
        context.topic_subscriber = context.topic and (context.authemail in context.topic['subscribers'])
        context.subscriber = context.forum_subscriber or context.topic_subscriber

        # Prepare other general context attributes.
        context.redirect_url = None
        context.format = context.req.args.get('format')

    def _get_actions(self, context):
        # Get action.
        action = context.req.args.get('discussion_action')
        preview = context.req.args.has_key('preview')
        submit = context.req.args.has_key('submit')
        self.log.debug('realm: %s, action: %s, format: %s preview: %s, submit:'
                       ' %s' % (context.realm, action, context.format, preview, submit))

        # Determine mode.
        if context.message:
            if context.realm == 'discussion-admin':
                pass
            elif context.realm == 'discussion-ajax':
                if action == 'edit-attribute':
                    return ['message-edit-attribute']
                elif action == 'subscribe':
                    return ['topic-subscribe']
                elif action == 'unsubscribe':
                    return ['topic-unsubscribe']
            elif context.realm == 'discussion-wiki':
                if action == 'add':
                    return ['message-add', 'wiki-message-list']
                elif action == 'quote':
                    return ['message-quote', 'wiki-message-list']
                elif action == 'post-add':
                    if preview:
                        return ['wiki-message-list']
                    else:
                        return ['message-post-add']
                elif action == 'edit':
                    return ['message-edit', 'wiki-message-list']
                elif action == 'post-edit':
                    if preview:
                        return ['wiki-message-list']
                    else:
                        return ['message-post-edit']
                elif action == 'delete':
                    return ['message-delete']
                elif action == 'set-display':
                    return ['topic-set-display', 'wiki-message-list']
                else:
                    return ['wiki-message-list']
            else:
                if context.format == 'rss':
                    return ['topic-rss']
                if action == 'add':
                    return ['message-add', 'message-list']
                elif action == 'quote':
                    return ['message-quote', 'message-list']
                elif action == 'post-add':
                    if preview:
                        return ['message-list']
                    else:
                        return ['message-post-add']
                elif action == 'edit':
                    return ['message-edit', 'message-list']
                elif action == 'post-edit':
                    if preview:
                        return ['message-list']
                    else:
                        return ['message-post-edit']
                elif action == 'delete':
                    return ['message-delete']
                elif action == 'set-display':
                    return ['topic-set-display', 'message-list']
                else:
                    return ['message-list']
        if context.topic:
            if context.realm == 'discussion-admin':
                pass
            elif context.realm == 'discussion-ajax':
                if action == 'edit-attribute':
                    return ['topic-edit-attribute']
                elif action == 'subscribe':
                    return ['topic-subscribe']
                elif action == 'unsubscribe':
                    return ['topic-unsubscribe']
                elif action == 'subscribe-toggle':
                    return ['topic-toggle-subscribe']
            elif context.realm == 'discussion-wiki':
                if action == 'add':
                    return ['message-add', 'wiki-message-list']
                elif action == 'quote':
                    return ['topic-quote', 'wiki-message-list']
                elif action == 'post-add':
                    if preview:
                        return ['wiki-message-list']
                    else:
                        return ['message-post-add']
                elif action == 'edit':
                    return ['topic-edit', 'wiki-message-list']
                elif action == 'post-edit':
                    if preview:
                        return ['wiki-message-list']
                    else:
                        return ['topic-post-edit']
                elif action == 'set-display':
                    return ['topic-set-display', 'wiki-message-list']
                elif action == 'subscriptions-post-add':
                    return ['topic-subscriptions-post-add']
                elif action == 'subscriptions-post-edit':
                    return ['topic-subscriptions-post-edit']
                else:
                    return ['wiki-message-list']
            else:
                if context.format == 'rss':
                    return ['topic-rss']
                if action == 'add':
                    return ['message-add', 'message-list']
                elif action == 'quote':
                    return ['topic-quote', 'message-list']
                elif action == 'post-add':
                    if preview:
                        return ['message-list']
                    else:
                        return ['message-post-add']
                elif action == 'edit':
                    return ['topic-edit', 'message-list']
                elif action == 'post-edit':
                    if preview:
                        return ['message-list']
                    else:
                        return ['topic-post-edit']
                elif action == 'delete':
                    return ['topic-delete']
                elif action == 'move':
                    return ['topic-move']
                elif action == 'post-move':
                    return ['topic-post-move']
                elif action == 'set-display':
                    return ['topic-set-display', 'message-list']
                elif action == 'subscriptions-post-add':
                    return ['topic-subscriptions-post-add']
                elif action == 'subscriptions-post-edit':
                    return ['topic-subscriptions-post-edit']
                else:
                    return ['message-list']
        elif context.forum:
            if context.realm == 'discussion-admin':
                if action == 'post-edit':
                    return ['forum-post-edit']
                else:
                    return ['admin-forum-list']
            elif context.realm == 'discussion-ajax':
                if action == 'edit-attribute':
                    return ['forum-edit-attribute']
                elif action == 'subscribe':
                    return ['forum-subscribe']
                elif action == 'unsubscribe':
                    return ['forum-unsubscribe']
                elif action == 'subscribe-toggle':
                    return ['forum-toggle-subscribe']
            elif context.realm == 'discussion-wiki':
                return ['wiki-message-list']
            else:
                if context.format == 'rss':
                    return ['forum-rss']
                if action == 'add':
                    return ['topic-add']
                elif action == 'post-add':
                    if preview:
                        return ['topic-add']
                    else:
                        return ['topic-post-add']
                elif action == 'delete':
                    return ['forum-delete']
                elif action == 'set-display':
                    return ['forum-set-display', 'topic-list']
                elif action == 'subscriptions-post-edit':
                    return ['forum-subscriptions-post-edit']
                else:
                    return ['topic-list']
        elif context.group:
            if context.realm == 'discussion-admin':
                if action == 'post-add':
                    return ['forum-post-add']
                elif action == 'post-edit':
                    return ['group-post-edit']
                elif action == 'delete':
                    return ['forums-delete']
                else:
                    if context.group['id']:
                        return ['admin-group-list']
                    else:
                        return ['admin-forum-list']
            elif context.realm == 'discussion-ajax':
                if action == 'edit-attribute':
                    return ['group-edit-attribute']
            elif context.realm == 'discussion-wiki':
                return ['wiki-message-list']
            else:
                if action == 'post-add':
                    return ['forum-post-add']
                else:
                    return ['forum-list']
        else:
            if context.realm == 'discussion-admin':
                if action == 'post-add':
                    return ['group-post-add']
                elif action == 'delete':
                    return ['groups-delete']
                else:
                    return ['admin-group-list']
            elif context.realm == 'discussion-wiki':
                return ['wiki-message-list']
            else:
                if action == 'add':
                    return ['forum-add']
                elif action == 'post-add':
                    return ['forum-post-add']
                else:
                    return ['forum-list']

    def _get_template(self, context, actions):
        if context.format == 'rss':
            return actions[-1].replace('-rss', '') + '.rss'
        else:
            return actions[-1] + '.html'

    def _do_actions(self, context, actions):
        for action in actions:
            if action == 'group-list':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Display groups.
                context.data['groups'] = self.get_groups(context)

            elif action == 'admin-group-list':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get form values.
                order = context.req.args.get('order', 'id')
                desc = context.req.args.get('desc') == '1'

                # Display groups.
                context.data['order'] = order
                context.data['desc'] = desc
                context.data['groups'] = self.get_groups(context, order, desc)

            elif action == 'group-add':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

            elif action == 'group-post-add':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get form values.
                group = {'name' : context.req.args.get('name'),
                         'description' : context.req.args.get('description')}

                # Add new group.
                self.add_group(context, group)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'group-post-edit':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get form values.
                group = {'name' : context.req.args.get('name'),
                         'description' : context.req.args.get('description')}

                # Edit group.
                self.edit_group(context, context.group['id'], group)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'group-delete':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'groups-delete':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get selected groups.
                selection = context.req.args.get('selection')
                if isinstance(selection, (str, unicode)):
                    selection = [selection]

                # Delete selected groups.
                if selection:
                    for group_id in selection:
                        self.delete_group(context, int(group_id))

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'forum-list':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Get form values.
                order = context.req.args.get('order') or self.forum_sort

                desc = 1
                if order == 'subject' or order == 'moderator':
                    desc = 0

                # Display forums.
                context.data['order'] = order
                context.data['desc'] = desc
                context.data['groups'] = self.get_groups(context)
                context.data['forums'] = self.get_forums(context, order, desc)

            elif action == 'admin-forum-list':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get ordering arguments values.
                order = context.req.args.get('order') or self.forum_sort
                if context.req.args.has_key('desc'):
                    desc = context.req.args.get('desc') == '1'
                else:
                    desc = self.forum_sort_direction

                # Display forums.
                context.data['order'] = order
                context.data['desc'] = desc
                context.data['groups'] = self.get_groups(context)
                context.data['forums'] = self.get_forums(context, order, desc)

            elif action == 'forum-rss':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Get topics and messges.
                messages = self.get_flat_messages_by_forum(
                    context, context.forum['id'], desc = True, limit = self.messages_per_page)
                topics = self.get_topics(
                    context, context.forum['id'], desc = True, limit = self.topics_per_page)

                # Create map of topic subjects.
                topic_subjects = {}
                for message in messages:
                    if not topic_subjects.has_key(message['topic']):
                        topic_subjects[message['topic']] = \
                            self.get_topic_subject(context, message['topic'])

                # Prepare list of topics and messages of the forum.
                context.data['topics'] = topics
                context.data['messages'] = messages
                context.data['topic_subjects'] = topic_subjects

            elif action == 'forum-add':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Display Add Forum form.
                context.data['groups'] = self.get_groups(context)

            elif action == 'forum-post-add':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # restrict user creating NEWS forum, if that is defined in configuration
                if conf.news_forum_name and context.req.args.get('name') == conf.news_forum_name:
                    raise TracError("Forum name '" + conf.news_forum_name + "' is reserved, please use another name")

                # Get form values
                forum = {'name' : context.req.args.get('name'),
                         'author' : context.req.authname,
                         'subject' : context.req.args.get('subject'),
                         'description' : context.req.args.get('description'),
                         'moderators' : self.get_moderators(context),
                         'subscribers' : self.get_subscribers(context, True),
                         'forum_group' : int(context.req.args.get('group') or 0),
                         'time': to_timestamp(datetime.now(utc)),
                         'tags': context.req.args.get('tags')}

                # Fix tags attribute to be a list
                if not forum['tags']:
                    forum['tags'] = []
                if not isinstance(forum['tags'], list):
                    forum['tags'] = [tag.strip() for tag in
                                     forum['tags'].replace(',', ' ').split()]

                # Perform new forum add.
                self.add_forum(context, forum)

                # Get inserted forum with new ID.
                context.forum = self.get_forum_by_time(context, forum['time'])

                # Copy tags field which is not stored in the database table.
                context.forum['tags'] = forum['tags']

                # Notify change listeners.
                self.log.debug('forum_change_listeners: %s' % (self.forum_change_listeners))
                for listener in self.forum_change_listeners:
                    listener.forum_created(context, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'forum-post-edit':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get form values.
                forum = {'name' : context.req.args.get('name'),
                         'subject' : context.req.args.get('subject'),
                         'description' : context.req.args.get('description'),
                         'moderators' : self.get_moderators(context),
                         'subscribers' : self.get_subscribers(context, True),
                         'forum_group' : int(context.req.args.get('group') or 0)}

                # Perform forum edit.
                self.edit_forum(context, context.forum['id'], forum)

                # Notify change listeners.
                for listener in self.forum_change_listeners:
                    listener.forum_changed(context, forum, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'forum-delete':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # restrict user deleting NEWS forum, if that is defined in configuration
                if conf.news_forum_name and context.forum['name'] == conf.news_forum_name:
                    raise TracError("Forum name '" + conf.news_forum_name + "' cannot be deleted.")

                # Delete forum.
                self.delete_forum(context, context.forum['id'])

                # Notify change listeners.
                for listener in self.forum_change_listeners:
                    listener.forum_deleted(context, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'forums-delete':
                context.req.perm.assert_permission('DISCUSSION_ADMIN')

                # Get selected forums.
                selection = context.req.args.get('selection')
                if isinstance(selection, (str, unicode)):
                    selection = [selection]

                news_forum_id = self._get_news_forum_id (context)
                if news_forum_id and str(news_forum_id) in selection:
                    selection.remove (str(news_forum_id))
                    add_notice(context.req, "Announcements forum can't be deleted")

                # Delete selected forums.
                if selection:
                    for forum_id in selection:
                        # Delete forum.
                        self.delete_forum(context, int(forum_id))

                        # Notify change listeners.
                        for listener in self.forum_change_listeners:
                            listener.forum_deleted(context, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'forum-set-display':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Get form values.
                display = context.req.args.get('display')

                # Set message list display mode to session.
                context.req.session['topic-list-display'] = display

            elif action == 'forum-subscriptions-post-edit':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Prepare edited attributes of the forum.
                forum = {'subscribers' : self.get_subscribers(context, False)}

                # Edit topic.
                self.edit_forum(context, context.forum['id'], forum)

                # Notify change listeners.
                for listener in self.forum_change_listeners:
                    listener.forum_changed(context, forum, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'forum-subscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail and not context.req.authname in context.forum['subscribers']:
                    # Prepare edited attributes of the forum.
                    forum = {'subscribers' : deepcopy(context.forum['subscribers'])}
                    forum['subscribers'].append(context.req.authname)

                    # Edit topic.
                    self.edit_forum(context, context.forum['id'], forum)

                    # Notify change listeners.
                    for listener in self.forum_change_listeners:
                        listener.forum_changed(context, forum, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'forum-unsubscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail and context.req.authname in context.forum['subscribers']:

                    # Prepare edited attributes of the topic.
                    forum = {'subscribers' : deepcopy(context.forum['subscribers'])}
                    forum['subscribers'].remove(context.req.authname)

                    # Edit topic.
                    self.edit_forum(context, context.forum['id'], forum)

                    # Notify change listeners.
                    for listener in self.forum_change_listeners:
                        listener.forum_changed(context, forum, context.forum)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'forum-toggle-subscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail:
                    if not context.req.authname in context.forum['subscribers'] and not \
                           context.authemail in context.forum['subscribers']:

                        # Prepare edited attributes of the forum.
                        forum = {'subscribers' : deepcopy(context.forum['subscribers'])}
                        forum['subscribers'].append(context.req.authname)

                        # Edit topic.
                        self.edit_forum(context, context.forum['id'], forum)

                        # Notify change listeners.
                        for listener in self.forum_change_listeners:
                            listener.forum_changed(context, forum, context.forum)

                        context.data['buttonstring'] = "Unsubscribe"

                    elif context.req.authname in context.forum['subscribers'] or \
                         context.authemail in context.forum['subscribers']:

                        # Prepare edited attributes of the topic.
                        forum = {'subscribers' : deepcopy(context.forum['subscribers'])}
                        if context.req.authname in context.forum['subscribers']:
                            forum['subscribers'].remove(context.req.authname)

                        if context.authemail in context.forum['subscribers']:
                            forum['subscribers'].remove(context.authemail)

                        # Edit topic.
                        self.edit_forum(context, context.forum['id'], forum)

                        # Notify change listeners.
                        for listener in self.forum_change_listeners:
                            listener.forum_changed(context, forum, context.forum)

                        context.data['buttonstring'] = "Subscribe"

#                # Redirect request to prevent re-submit.
#                context.redirect_url = (context.req.path_info, '#subscriptions')


            elif action == 'topic-list':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Update this forum visit time.
                context.visited_forums[context.forum['id']] = to_timestamp(datetime.now(utc))

                # Get form values.
                order = context.req.args.get('order') or self.topic_sort

                desc = 1
                if order == 'subject' or order == 'author':
                    desc = 0

                page = (safe_int(context.req.args.get('discussion_page') or '1') or 1) -1

                # Get topic list display type from session.
                display = context.req.session.get('topic-list-display') or self.default_topic_display

                # Get user's row count on page
                store = get_userstore()
                user = store.getUser(context.req.authname)

                # Let's use the configured default as first guess
                topics_per_page = self.topics_per_page

                # Does the user want to change the rowcount?
                rowsperpage = safe_int(context.req.args.get('rowsperpage') or '0') or 0

                if rowsperpage > 0 and context.req.authname != 'anonymous':
                    user.preferences['topics_per_page'] = rowsperpage
                    store.updateUser(user)
                    topics_per_page = rowsperpage

                # if user is anonymous, use cookies instead of prefs
                if context.req.authname == 'anonymous':
                    if rowsperpage > 0:
                        context.req.session['topics_per_page'] = str(rowsperpage)
                        topics_per_page = rowsperpage
                    elif context.req.session.has_key('topics_per_page'):
                        topics_per_page = int(context.req.session['topics_per_page'])

                # not anonymous, use his/her preference
                else:
                    topics_per_page = (user.preferences.has_key('topics_per_page') and
                                       int(user.preferences['topics_per_page']) or self.topics_per_page)

                messages_per_page = self.messages_per_page
                if context.req.authname == 'anonymous':
                    if context.req.session.has_key('messages_per_page'):
                        messages_per_page = int(context.req.session['messages_per_page'])
                else:
                    messages_per_page = (user.preferences.has_key('messages_per_page') and
                                         int(user.preferences['messages_per_page']) or
                                         self.messages_per_page)

                # Get topics of current page.
                topics_count = self.get_topics_count(context, context.forum['id'])
                topics = self.get_topics(context, context.forum['id'], order, desc, topics_per_page,
                                         page * topics_per_page, False)
                paginator = self._get_paginator(context, page, topics_per_page, topics_count)

                # Display topics.
                context.data['order'] = order
                context.data['desc'] = desc
                context.data['display'] = display
                context.data['topics'] = topics
                context.data['paginator'] = paginator
                context.data['messages_per_page'] = messages_per_page

            elif action == 'topic-rss':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Display list of messages for topic.
                context.data['messages'] = self.get_flat_messages(
                    context, context.topic['id'], desc = True, limit = self.messages_per_page)

            elif action == 'topic-add':
                context.req.perm.assert_permission('DISCUSSION_APPEND')
                if context.is_news_forum:
                    if not context.perm_create_news and not context.perm_append_news:
                        raise PermissionError("You have no permission to write news")


            elif action == 'topic-quote':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Prepare old content.
                lines = context.topic['body'].splitlines()
                for I in xrange(len(lines)):
                    lines[I] = '> %s' % (lines[I])
                context.req.args['body'] = '\n'.join(lines)

            elif action == 'topic-post-add':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Get form values.
                priority = context.req.args.get('important')
                if priority == None:
                    priority = 0

                topic = {'forum' : context.forum['id'],
                         'subject' : context.req.args.get('subject'),
                         'time': to_timestamp(datetime.now(utc)),
                         'author' : context.req.authname,
                         'subscribers' : self.get_subscribers(context, True),
                         'body' : context.req.args.get('body'),
                         'priority' : priority }

                # Add user e-mail if subscription checked.
                if context.req.args.get('subscribe') and context.authemail and \
                       not context.req.authname in topic['subscribers']:
                    topic['subscribers'].append(context.req.authname)

                # Filter topic.
                for discussion_filter in self.discussion_filters:
                    self.log.debug('filtering topic: %s' % (topic,))
                    accept, topic_or_error = discussion_filter.filter_topic(context, topic)
                    if accept:
                        topic = topic_or_error
                    else:
                        raise TracError(topic_or_error)

                # Add new topic.
                self.add_topic(context, topic)

                # Get inserted topic with new ID.
                context.topic = self.get_topic_by_time(context, topic['time'])

                # Notify change listeners.
                self.log.debug('topic_change_listeners: %s' % (self.topic_change_listeners))
                for listener in self.topic_change_listeners:
                    listener.topic_created(context, context.topic)

                # Redirect request to prevent re-submit.
                if context.realm != 'discussion-wiki':
                    href = Href('discussion')
                    context.redirect_url = (href('topic', context.topic['id']), '#topic')
                else:
                    context.redirect_url = (context.req.path_info, '#topic')

            elif action == 'topic-edit':
                context.req.perm.assert_permission('DISCUSSION_APPEND')
                if not context.moderator and context.topic['author'] != context.req.authname:
                    raise PermissionError('Topic edit')

                # Prepare form values.
                context.req.args['subject'] = context.topic['subject']
                context.req.args['body'] = context.topic['body']

            elif action == 'topic-post-edit':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Check if user can edit topic.
                if not context.moderator and context.topic['author'] != context.req.authname:
                    raise PermissionError('Topic editing')

                # Check if user can edit locked topic.
                if not context.moderator and 'locked' in context.topic['status']:
                    raise PermissionError("Locked topic editing")

                # Get form values.
                topic = {'subject' : context.req.args.get('subject'),
                         'body' : context.req.args.get('body')}

                # Edit topic.
                self.edit_topic(context, context.topic['id'], topic)

                # Notify change listeners.
                for listener in self.topic_change_listeners:
                    listener.topic_changed(context, topic, context.topic)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#topic')

            elif action == 'topic-edit-attribute':
                # Check general topic editing permission.
                context.req.perm.assert_permission('DISCUSSION_APPEND')
                if not context.moderator and context.topic['author'] != context.req.authname:
                    raise PermissionError("Topic editing")

                # Get form values.
                if not context.req.args.has_key('name') and context.req.args.has_key('value'):
                    raise TracError("Missing request arguments.")
                name = context.req.args.get('name')
                value = context.req.args.get('value')

                # Important flag is implemented as integer priority.
                if name == 'important':
                    name = 'priority'
                    value = 1 if value in ('true', 'yes', True) else 0

                # Attributes that can be changed only by administrator.
                topic = {}
                if name in ('id', 'time'):
                    context.req.perm.assert_permission('DISCUSSION_ADMIN')
                    topic[name] = value
                # Attributes that can be changed by moderator.
                elif name in ('forum', 'author', 'subscribers', 'priority',
                              'status.locked', 'status'):
                    context.req.perm.assert_permission('DISCUSSION_MODERATE')
                    if not context.moderator:
                        raise PermissionError("Topic editing")

                    # Decode status flag to status list.
                    if name in ('status.locked'):
                        topic['status'] = context.topic['status'].copy()
                        if value in ('true', 'yes', True):
                            topic['status'] |= set(['locked'])
                        else:
                            topic['status'] -= set(['locked'])
                    else:
                        topic[name] = value

                # Attributes that can be changed by owner of the topic or the
                # moderator.
                elif name in ('subject', 'body', 'status.solved'):
                    self.log.debug((context.topic['author'], context.req.authname))
                    context.req.perm.assert_permission('DISCUSSION_APPEND')

                    # Check if user can edit topic.
                    if not context.moderator and context.topic['author'] != context.req.authname:
                        raise PermissionError("Topic editing")

                    # Decode status flag to status list.
                    if name in ('status.solved'):
                        topic['status'] = context.topic['status'].copy()
                        if value in ('true', 'yes', True):
                            topic['status'] |= set(['solved'])
                            topic['status'] -= set(['unsolved'])
                        else:
                            topic['status'] |= set(['unsolved'])
                            topic['status'] -= set(['solved'])
                    else:
                        topic[name] = value
                else:
                    raise PermissionError("Topic editing")

                # Update the attribute value.
                self.edit_topic(context, context.topic['id'], topic)

            elif action == 'topic-move':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Display Move Topic form.
                context.data['forums'] = self.get_forums(context)

            elif action == 'topic-post-move':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Get form values.
                forum_id = int(context.req.args.get('new_forum') or 0)

                # Move topic.
                self.set_forum(context, context.topic['id'], forum_id)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '')

            elif action == 'topic-delete':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Delete topic.
                self.delete_topic(context, context.topic['id'])

                # Notify change listeners.
                for listener in self.topic_change_listeners:
                    listener.topic_deleted(context, context.topic)

                # Redirect request to prevent re-submit.
                if context.realm != 'discussion-wiki':
                    href = Href('discussion')
                    context.redirect_url = (href('forum',
                      context.topic['forum']), '')
                else:
                    context.redirect_url = (context.req.path_info, '')

            elif action == 'topic-set-display':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Get form values.
                display = context.req.args.get('display')

                # Set message list display mode to session.
                context.req.session['message-list-display'] = display

            elif action == 'topic-subscriptions-post-edit':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Prepare edited attributes of the forum.
                topic = {'subscribers' : self.get_subscribers(context, False)}

                # Edit topic.
                self.edit_topic(context, context.topic['id'], topic)

                # Notify change listeners.
                for listener in self.topic_change_listeners:
                    listener.topic_changed(context, topic, context.topic)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'topic-subscriptions-post-add':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                # Prepare edited attributes of the forum..
                topic = {'subscribers' : context.topic['subscribers']}

                # Get subscribers from request and add them to subscribers, unless they're already
                # present. get_subscribers would throw this error on it's own, but not this call,
                # so we need to validate the subscribers on our own.
                subscribers = self.unpack_users(
                    context.req.args.get('subscribers'), topic['subscribers'])
                for sub in subscribers:
                    if len(sub) > 150:
                        raise TracError('Subscriber username or email "%s" is too '
                                        'long (more than 150 characters)' % sub)

                topic['subscribers'] += subscribers

                # Edit topic.
                self.edit_topic(context, context.topic['id'], topic)

                # Notify change listeners.
                for listener in self.topic_change_listeners:
                    listener.topic_changed(context, topic, context.topic)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'topic-subscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail and not context.req.authname in context.topic['subscribers']:
                    # Prepare edited attributes of the topic.
                    topic = {'subscribers' : deepcopy(context.topic['subscribers'])}
                    topic['subscribers'].append(context.req.authname)

                    # Edit topic.
                    self.edit_topic(context, context.topic['id'], topic)

                    # Notify change listeners.
                    for listener in self.topic_change_listeners:
                        listener.topic_changed(context, topic, context.topic)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'topic-unsubscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail and context.req.authname in context.topic['subscribers']:
                    # Prepare edited attributes of the topic.
                    topic = {'subscribers' : deepcopy(context.topic['subscribers'])}
                    topic['subscribers'].remove(context.req.authname)

                    # Edit topic.
                    self.edit_topic(context, context.topic['id'], topic)

                    # Notify change listeners.
                    for listener in self.topic_change_listeners:
                        listener.topic_changed(context, topic, context.topic)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'topic-toggle-subscribe':
                context.req.perm.assert_permission('DISCUSSION_VIEW')

                if context.authemail:
                    if not context.req.authname in context.topic['subscribers'] and not \
                           context.authemail in context.topic['subscribers']:
                        # Prepare edited attributes of the topic.
                        topic = {'subscribers' : deepcopy(context.topic['subscribers'])}
                        topic['subscribers'].append(context.req.authname)

                        # Edit topic.
                        self.edit_topic(context, context.topic['id'], topic)

                        # Notify change listeners.
                        for listener in self.topic_change_listeners:
                            listener.topic_changed(context, topic, context.topic)
                        context.data['buttonstring'] = "Unsubscribe"
                    elif context.req.authname in context.topic['subscribers'] or \
                         context.authemail in context.topic['subscribers']:
                        # Prepare edited attributes of the topic.
                        topic = {'subscribers' : deepcopy(context.topic['subscribers'])}

                        if context.req.authname in context.topic['subscribers']:
                            topic['subscribers'].remove(context.req.authname)
                        if context.authemail in context.topic['subscribers']:
                            topic['subscribers'].remove(context.authemail)

                        # Edit topic.
                        self.edit_topic(context, context.topic['id'], topic)

                        # Notify change listeners.
                        for listener in self.topic_change_listeners:
                            listener.topic_changed(context, topic, context.topic)

                        context.data['buttonstring'] = "Subscribe"

                # Redirect request to prevent re-submit.
#                context.redirect_url = (context.req.path_info, '#subscriptions')

            elif action == 'message-list':
                context.req.perm.assert_permission('DISCUSSION_VIEW')
                self._prepare_message_list(context, context.topic)

            elif action == 'wiki-message-list':
                if context.topic:
                    self._prepare_message_list(context, context.topic)

            elif action == 'message-add':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

            elif action == 'message-quote':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Prepare old content.
                lines = context.message['body'].splitlines()
                for I in xrange(len(lines)):
                    lines[I] = '> %s' % (lines[I])
                context.req.args['body'] = '\n'.join(lines)

            elif action == 'message-post-add':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Check if user can post to locked topic.
                if not context.moderator and 'locked' in context.topic['status']:
                    raise PermissionError("Locked topic posting")

                if context.is_news_forum:
                    if not context.perm_append_news and not context.perm_create_news:
                        raise PermissionError("You have no permission to comment news")

                # Get form values.
                message = {'forum' : context.forum['id'],
                           'topic' : context.topic['id'],
                           'replyto' : context.message and context.message['id'] or - 1,
                           'author' : context.req.authname,
                           'body' : context.req.args.get('body'),
                           'time' : to_timestamp(datetime.now(utc))}

                # Filter message.
                for discussion_filter in self.discussion_filters:
                    self.log.debug('filtering message: %s' % (message,))
                    accept, message_or_error = discussion_filter.filter_message(
                        context, message)
                    if accept:
                        message = message_or_error
                    else:
                        raise TracError(message_or_error)

                # Add message.
                self.add_message(context, message)

                # Get inserted message with new ID.
                context.message = self.get_message_by_time(context, message['time'])

                # Notify change listeners.
                self.log.debug('message_change_listeners: %s' % (self.message_change_listeners))
                for listener in self.message_change_listeners:
                    listener.message_created(context, context.message)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info,
                                        '#message%s' % (context.message['id'],))

            elif action == 'message-edit':
                context.req.perm.assert_permission('DISCUSSION_APPEND')
                if not context.moderator and context.message['author'] != context.req.authname:
                    raise PermissionError('Message edit')

                # Prepare form values.
                context.req.args['body'] = context.message['body']

            elif action == 'message-post-edit':
                context.req.perm.assert_permission('DISCUSSION_APPEND')

                # Check if user can edit message.
                if not context.moderator and context.message['author'] != context.req.authname:
                    raise PermissionError('Message edit')

                # Check if user can edit locked topic.
                if not context.moderator and 'locked' in context.topic['status']:
                    raise PermissionError("Locked topic editing")

                # Get form values.
                message = {'body' : context.req.args.get('body')}

                # Edit message.
                self.edit_message(context, context.message['id'], message)

                # Notify change listeners.
                for listener in self.message_change_listeners:
                    listener.message_changed(context, message, context.message)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info,
                                        '#message%s' % (context.message['id'],))

            elif action == 'message-delete':
                context.req.perm.assert_permission('DISCUSSION_MODERATE')
                if not context.moderator:
                    raise PermissionError('Forum moderate')

                # Delete message.
                self.delete_message(context, context.message['id'])

                # Notify change listeners.
                for listener in self.message_change_listeners:
                    listener.message_deleted(context, context.message)

                # Redirect request to prevent re-submit.
                context.redirect_url = (context.req.path_info,
                                        '#message%s' % (context.message['replyto'],))

        # Redirection is not necessary.
        return None

    def _prepare_message_list(self, context, topic):
        # Get time when topic was visited from session.

        if not topic:
            visit_time = 0
        else:
            visit_time = int(context.visited_topics.has_key(topic['id']) and
                             (context.visited_topics[topic['id']] or 0))

        # Get form values
        page = (safe_int(context.req.args.get('discussion_page') or '1') or 1) -1

        # Update this topic visit time.
        if topic:
            context.visited_topics[topic['id']] = to_timestamp(datetime.now(utc))

        # Get topic messages for the current page.
        display = context.req.session.get('message-list-display') or \
          self.default_message_display

        # Get user's row count on page
        store = get_userstore()
        user = store.getUser(context.req.authname)

        # Let's use the configured default as first guess
        messages_per_page = self.messages_per_page

        # Does the user want to change the rowcount?
        rowsperpage = safe_int(context.req.args.get('rowsperpage') or '0') or 0

        # if does, store the value into user's prefs
        if rowsperpage > 0 and context.req.authname != 'anonymous':
            user.preferences['messages_per_page'] = rowsperpage
            store.updateUser(user)
            messages_per_page = rowsperpage

        # if user is anonymous, use cookies instead of prefs
        if context.req.authname == 'anonymous':
            if rowsperpage > 0:
                context.req.session['messages_per_page'] = str(rowsperpage)
                messages_per_page = rowsperpage
            elif context.req.session.has_key('messages_per_page'):
                messages_per_page = int(context.req.session['messages_per_page'])

        # not anonymous, use his/her preference
        else:
            messages_per_page = (user.preferences.has_key('messages_per_page') and
                                 int(user.preferences['messages_per_page']) or self.messages_per_page)

        if topic:
            messages_count = self.get_messages_count(context, topic['id'])
            # the topic is counted also as a message
            messages_count = messages_count + 1
            if page == 0:
                messages = self.get_flat_messages(
                    context, topic['id'], desc = False, limit = messages_per_page - 1,
                    offset = page * messages_per_page)
            else:
                messages = self.get_flat_messages(
                    context, topic['id'], desc = False, limit = messages_per_page,
                    offset = page * messages_per_page - 1)

        # Create paginator. Splitted because of topic on first page leaving only 9 slots for replies.
        paginator = self._get_paginator(
            context, page, messages_per_page, topic and messages_count or 1, anchor = '#topic')

        # Prepare display of messages.
        context.data['visit_time'] = visit_time
        context.data['display'] = display
        context.data['messages'] = topic and messages or []
        context.data['paginator'] = paginator
        context.data['msg_counts'] = self.get_users_message_count(context)
        context.data['visualstatus'] = self._topic_status_to_visual_list(
            topic['status'], topic['priority'])

        # Create context with topic resource for attachments.
        topic_context = Context.from_request(context.req)
        topic_context.realm = context.realm
        topic_context.resource = Resource('discussion',
                                          'topic/%s' % (topic and context.topic['id'] or 0,))

        # Display list of attachments.
        context.data['attachments'] = AttachmentModule(self.env).attachment_data(topic_context)

    def _get_paginator(self, context, page, items_limit, items_count, anchor = ''):
        # Create paginator object.
        paginator = Paginator([], page, items_limit, items_count)

        # Initialize pages.
        page_data = []
        shown_pages = paginator.get_shown_pages(21)
        for shown_page in shown_pages:
            page_data.append([
                context.req.href(context.req.path_info, discussion_page = shown_page) + anchor,
                None, to_unicode(shown_page), 'page %s' % (shown_page,)])
        fields = ['href', 'class', 'string', 'title']
        paginator.shown_pages = [dict(zip(fields, p)) for p in page_data]

        paginator.current_page = {'href' : None,
                                  'class' : 'current',
                                  'string': str(page + 1),
                                  'title' : None}

        # Prepare links to next or previous page.
        if paginator.has_next_page:
            add_link(
                context.req, 'next', context.req.href(
                    context.req.path_info, discussion_page = paginator.page + 2) + anchor, 'Next Page')
        if paginator.has_previous_page:
            add_link(
                context.req, 'prev', context.req.href(
                    context.req.path_info, discussion_page = paginator.page) + anchor, 'Previous Page')

        return paginator

    # Get one item functions.

    def _get_item(self, context, table, columns, where='', values=()):
        """
        Gets an item from database.

        :param Context context: The Trac request based context.
        :param str table: The item to get, as a table name.
        :param tuple columns: Tuple of columns to get for the item.
        :param str where: Column to limit the search by, works together with param values
        :param tuple values: Tuple of values
        :returns: Dict of column => value pairs for the given table
        """
        if not table in self.VALID_TABLES:
            raise TracError("Cannot get item %s" % table)

        for column in columns:
            if column not in self.VALID_COLUMNS:
                raise TracError("Invalid column %s in getting an item" % column)

        if where and where not in self.VALID_COLUMNS:
            raise TracError("Invalid selection %s" % where)

        if where:
            sql = '''
                SELECT %s FROM %s
                WHERE %s = %%s
            ''' % (', '.join(columns), table, where)
        else:
            sql = "SELECT %s FROM %s" % (', '.join(columns), table)

        self.log.debug(sql % values)
        context.cursor.execute(sql, values)
        for row in context.cursor:
            row = dict(zip(columns, row))
            return row
        return None

    def get_message(self, context, msg_id):
        # Get message by ID.
        return self._get_item(
            context, 'message', ('id', 'forum', 'topic', 'replyto', 'time', 'author', 'body'),
            where='id', values=(msg_id,))

    def get_message_by_time(self, context, time):
        # Get message by time of creation.
        return self._get_item(
            context, 'message', ('id', 'forum', 'topic', 'replyto', 'time', 'author', 'body'),
            where='time', values=(time,))

    def get_topic(self, context, topic_id):
        # Get topic by ID.
        topic = self._get_item(
            context, 'topic', ('id', 'forum', 'time', 'author', 'subscribers', 'subject',
                               'body', 'status', 'priority'), where='id', values=(topic_id,))

        # Unpack list of subscribers.
        if topic:
            topic['subscribers'] = self.unpack_users(topic['subscribers'])
            topic['unregistered_subscribers'] = []
            for subscriber in topic['subscribers']:
                if subscriber not in context.users:
                    topic['unregistered_subscribers'].append(subscriber)
            topic['status'] = self._topic_status_to_list(topic['status'])

        return topic

    def get_topic_by_time(self, context, time):
        # Get topic by time of creation.
        topic = self._get_item(
            context, 'topic', ('id', 'forum', 'time', 'author', 'subscribers', 'subject',
                               'body', 'status', 'priority'), where='time', values=(time,))

        # Unpack list of subscribers.
        if topic:
            topic['subscribers'] = self.unpack_users(topic['subscribers'])
            topic['unregistered_subscribers'] = []
            for subscriber in topic['subscribers']:
                if subscriber not in context.users:
                    topic['unregistered_subscribers'].append(subscriber)
            topic['status'] = self._topic_status_to_list(topic['status'])

        return topic

    def get_topic_by_subject(self, context, subject):
        # Get topic by subject.
        topic = self._get_item(
            context, 'topic', ('id', 'forum', 'time', 'author', 'subscribers', 'subject',
                               'body', 'status', 'priority'), where='subject', values=(subject,))

        # Unpack list of subscribers.
        if topic:
            topic['subscribers'] = self.unpack_users(topic['subscribers'])
            topic['unregistered_subscribers'] = []
            for subscriber in topic['subscribers']:
                if subscriber not in context.users:
                    topic['unregistered_subscribers'].append(subscriber)
            topic['status'] = self._topic_status_to_list(topic['status'])

        return topic

    def _topic_status_to_list(self, status):
        if status == 0 or status == None:
            return set(['unsolved'])
        status_list = set([])
        if status & 0x01:
            status_list.add('solved')
        else:
            status_list.add('unsolved')
        if status & 0x02:
            status_list.add('locked')
        return status_list

    def _topic_status_to_visual_list(self, status, priority):
        status_list = set([])
        if priority == 1:
            status_list.add(self.visual_status_tag_important)
        if 'solved' in status:
            status_list.add(self.visual_status_tag_solved)
        else:
            status_list.add(self.visual_status_tag_unsolved)
        if 'locked' in status:
            status_list.add(self.visual_status_tag_locked)
        return status_list

    def _topic_status_from_list(self, status_list):
        status = 0
        if 'solved' in status_list:
            status = status | 0x01
        if 'locked' in status_list:
            status = status | 0x02
        return status

    def get_forum(self, context, forum_id):
        # Get forum by ID.
        forum = self._get_item(
            context, 'forum', ('id', 'forum_group', 'name', 'subject', 'time', 'author',
                               'moderators', 'subscribers', 'description'), where='id',
            values=(forum_id,))

        # Unpack list of moderators and subscribers and get forum tags.
        if forum:
            forum['moderators'] = self.unpack_users(forum['moderators'])
            forum['subscribers'] = self.unpack_users(forum['subscribers'])
            forum['unregistered_subscribers'] = []
            for subscriber in forum['subscribers']:
                if subscriber not in context.users:
                    forum['unregistered_subscribers'].append(subscriber)
#TagSystem
#            if context.has_tags:
#                tag_system = TagSystem(self.env)
#                forum['tags'] = tag_system.get_tags(context.req,
#                  context.resource)

        return forum

    def get_forum_by_time(self, context, time):
        # Get forum by time of creation.
        forum = self._get_item(
            context, 'forum', ('id', 'forum_group', 'name', 'subject', 'time', 'author',
                               'moderators', 'subscribers', 'description'),
            where='time', values=(time,))

        # Unpack list of moderators and subscribers and get forum tags.
        if forum:
            forum['moderators'] = self.unpack_users(forum['moderators'])
            forum['subscribers'] = self.unpack_users(forum['subscribers'])
#TagSystem
#            if context.has_tags:
#                tag_system = TagSystem(self.env)
#                forum['tags'] = tag_system.get_tags(context.req,
#                  context.resource)

        return forum

    def get_group(self, context, grp_id):
        # Get forum group or none group.
        return self._get_item(
            context, 'forum_group', ('id', 'name', 'description'), where='id',
            values=(grp_id,)) or {'id' : 0, 'name': 'None', 'description': 'No Group'}

    # Get attribute functions.

    def get_topic_subject(self, context, topic_id):
        # Get subject of the topic.
        topic = self._get_item(context, 'topic', ('subject',), where='id', values=(topic_id,))
        return topic['subject']

    # Get items count functions.

    def _get_items_count(self, context, table, where='', values=()):
        """
        Get count of items in a table.

        :param Context context: The Trac request based context
        :param str table: The table name to count from
        :param str where: The column name to limit the count with
        :param tuple values: The values to match with where param
        :returns: Count of the items in question
        """
        if not table in self.VALID_TABLES:
            raise TracError("Cannot count items from table %s" % table)

        if where and where not in self.VALID_COLUMNS:
            raise TracError("Cannot limit count by column %s" % where)

        sql = None
        if where:
            sql = '''
                SELECT COUNT(id)
                FROM %s
                WHERE %s = %%s
            ''' % (table, where)
        else:
            sql = "SELECT COUNT(id) FROM %s" % table

        self.log.debug(sql % values)
        context.cursor.execute(sql, values)
        for row in context.cursor:
            return row[0]
        return 0

    def get_topics_count(self, context, forum_id):
        return self._get_items_count(context, 'topic', 'forum', (forum_id,))

    def get_messages_count(self, context, topic_id):
        return self._get_items_count(context, 'message', 'topic', (topic_id,))

    # Get list functions.

    def _get_items(self, context, table, columns, where='', values=(), order_by='',
                   desc=False, limit=0, offset=0):
        """
        Gets items from database. Result can be tuned with columns, where + value and
        order_by parameters.

        :param Context context: The Trac request based context
        :param str table: The table name to get the items from
        :param tuple columns: The table columns to get
        :param str where: The column to limit the search with
        :param tuple values: Tuple of values (of size 1) limit the search with
        :param str order_by: Column to order the results by
        :param bool desc: If set to True, the results will be sort in descending order
        :param int limit: Limit the number of results
        :param int offset: Search the results from nth item
        :returns: list of dicts of items, where keys will be the same as columns given in
        """
        if not table in self.VALID_TABLES:
            raise TracError("Cannot get items from table %s" % table)

        for column in columns:
            if column not in self.VALID_COLUMNS:
                raise TracError("Unknown column %s" % column)

        if where and where not in self.VALID_COLUMNS:
            raise TracError("Cannot filter results by column %s" % where)

        if order_by and order_by not in self.VALID_COLUMNS:
            raise TracError("Cannot sort by column %s" % order_by)

        sql_values = {'columns': ', '.join(columns),
                      'table': table,
                      'where': 'WHERE %s = %%s' % where if where else '',
                      'order_by': 'ORDER BY %s %s' % (order_by,
                                                      ('ASC', 'DESC')[bool(desc)]) if order_by else '',
                      'limit': 'LIMIT %s' % limit if limit else '',
                      'offset': 'OFFSET %s' % offset if offset else ''}

        sql = ("SELECT %(columns)s "
               "FROM %(table)s "
               "%(where)s "
               "%(order_by)s "
               "%(limit)s "
               "%(offset)s" % (sql_values))
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)
        items = []
        for row in context.cursor:
            row = dict(zip(columns, row))
            items.append(row)
        return items

    def get_groups(self, context, order_by='id', desc=False):
        """
        Gets set forum groups, sorted by a given in column.

        :param Context context: The Trac context containing request based data
        :param str order_by: Optional rule to determine which column is used in sorting. Must
            be id, name, description or forums
        :param bool desc: Optional parameter to determine if sorting should be ascending (False)
            or Descending (True)
        :raises: TracError for invalid arguments
        :returns: A list of dicts to describe the forum groups.
        """
        # Make sure only predefined order by's will be used
        if not order_by in ('id', 'name', 'description', 'forums'):
            raise TracError("Can't sort by %s" % order_by)

        # Get count of forums without group.
        sql = '''
            SELECT COUNT(f.id)
            FROM forum f
            WHERE f.forum_group = 0
        '''
        self.log.debug(sql)
        context.cursor.execute(sql)

        no_group_forums = 0
        for row in context.cursor:
            no_group_forums = row[0]

        groups = [{'id': 0,
                   'name': 'None',
                   'description': 'No Group',
                   'forums': no_group_forums}]

        # Get forum groups.
        if order_by != 'forums':
            order_by = 'g.' + order_by
        else:
            order_by = 'f.' + order_by

        columns = ('id', 'name', 'description', 'forums')
        sql_values = {'order_by': '', 'desc': ''}

        if order_by:
            sql_values = {'order_by': 'ORDER BY %s' % order_by,
                          'desc': ('ASC', 'DESC')[desc]}

        sql = '''
            SELECT g.id, g.name, g.description, f.forums
            FROM forum_group g
            LEFT JOIN
                (SELECT COUNT(id) AS forums, forum_group
                 FROM forum
                 GROUP BY forum_group) f
                ON g.id = f.forum_group
            %(order_by)s %(desc)s
        ''' % sql_values
        self.log.debug(sql)
        context.cursor.execute(sql)
        for row in context.cursor:
            row = dict(zip(columns, row))
            groups.append(row)
        return groups

    def get_forums(self, context, order_by='subject', desc=False):
        """
        Gets all available forums.
        """
        def _get_new_topic_count(context, forum_id):
            values = (forum_id, int(context.visited_forums.get(forum_id, 0)))
            sql = '''
                SELECT COUNT(id)
                FROM topic t
                WHERE t.forum = %s AND t.time > %s
            '''
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                return int(row[0])
            return 0

        def _get_new_replies_count(context, forum_id):
            sql = "SELECT id FROM topic t WHERE t.forum = %s"
            self.log.debug(sql % (forum_id,))
            context.cursor.execute(sql, (forum_id,))

            # Get IDs of topics in this forum.
            topics = []
            for row in context.cursor:
                topics.append(row[0])

            #Count unseen messages.
            count = 0
            for topic_id in topics:
                time = int(context.visited_topics.get(topic_id, 0))
                sql = "SELECT COUNT(id) FROM message m WHERE m.topic = %s AND m.time > %s"
                self.log.debug(sql % (topic_id, time))
                context.cursor.execute(sql, (topic_id, time))
                for row in context.cursor:
                    count += int(row[0])

            return count

        def _check_last_poster (context, forum_id):
            sql = "select max(time) from message where forum = %s"
            context.cursor.execute(sql, (forum_id,))
            lastreplytime = context.cursor.fetchone()

            sql = "select max(time) from topic where forum = %s"
            context.cursor.execute(sql, (forum_id,))
            lasttopictime = context.cursor.fetchone()

            if lasttopictime[0] == None:
                # no posts
                return (None, None)

            if lastreplytime[0] == None:
                # no replies, select last topic
                sql = "select author from topic where forum = %s and time = %s"
                context.cursor.execute(sql, (forum_id, lasttopictime[0]))
                lastposter = context.cursor.fetchone()
                return (lastposter[0], lasttopictime[0])

            if lasttopictime[0] > lastreplytime[0]:
                # last topic is newer, return it
                sql = "select author from topic where forum = %s and time = %s"
                context.cursor.execute(sql, (forum_id, lasttopictime[0]))
                lastposter = context.cursor.fetchone()
                return (lastposter[0], lasttopictime[0])

            if lastreplytime[0] >= lasttopictime[0]:
                # last reply is newer, return it
                sql = "select author from message where forum = %s and time = %s"
                context.cursor.execute(sql, (forum_id, lastreplytime[0]))
                lastposter = context.cursor.fetchone()
                return (lastposter[0], lastreplytime[0])

            return (None, None)

        # make sure only predefined order by's will be used
        if not order_by in ('id', 'topics', 'replies', 'lasttopic', 'lastreply', 'lastpost',
                            'lastposter', 'subject', 'viewcount'):
            raise TracError("Can't sort forums by %s" % order_by)

        if not order_by in ('topics', 'replies', 'lasttopic', 'lastreply', 'lastposter',
                            'lastpost'):
            order_by = 'f.%s' % order_by

        # case-insensitive order by
        if order_by in ('f.subject', 'lastposter'):
            order_by = 'lower(%s)' % order_by

        # this fetches the forum - topic level data
        columns = ('id', 'name', 'author', 'time', 'moderators', 'subscribers', 'forum_group',
                   'subject', 'description', 'topics', 'replies', 'viewcount', 'lastposttime',
                   'lastposter')

        sql = ("SELECT f.id, f.name, f.author, f.time, f.moderators, f.subscribers, f.forum_group, "
               "f.subject, f.description, ta.topics, ta.replies, ifnull(f.viewcount, 0), "
               "GREATEST(time,IFNULL(GREATEST(ta.lastreply,ta.lasttopic),0)) AS "
               "lastpost,IFNULL(IFNULL(ta.msgowner,ta.laststarter),author) AS lastposter "
               "FROM forum f "
               "LEFT JOIN ( "
               "SELECT COUNT(t.id) AS topics, MAX(t.time) AS lasttopic, t.author as laststarter, "
               "SUM(ma.replies) AS replies, "
               "MAX(ma.lastreply) AS lastreply, ma.owner AS msgowner, t.forum AS forum "
               "FROM topic t "
               "LEFT JOIN ( "
               "SELECT COUNT(m.id) AS replies, MAX(m.time) AS lastreply, m.topic AS topic, author AS owner "
               "FROM message m "
               "GROUP BY m.topic) ma ON t.id = ma.topic "
               "GROUP BY forum "
               ") ta ON f.id = ta.forum ")

        if not self.config.getbool('discussion', 'show_news_forum', 'true'):
            sql += "WHERE name <> '%s'" % conf.news_forum_name

        sql += "ORDER BY %s %s" % (order_by, ("ASC", "DESC")[bool(desc)])

        # this fetches the message level data, must be combiled to previous data
        #messagesql = "select forum, topic, max(time), author from message group by forum, topic order by time"

        context.cursor.execute(sql)

        # Convert certain forum attributes.
        forums = []
        for row in context.cursor:
            row = dict(zip(columns, row))
            forums.append(row)

        for forum in forums:
            # Compute count of new replies and topics.
            forum['new_topics'] = _get_new_topic_count(context, forum['id'])
            forum['new_replies'] = _get_new_replies_count(context, forum['id'])

            if forum['lastposttime'] == forum['time']:
                forum['lastposter'] = None
                forum['lastposttime'] = None

            lastposter, lastposttime = _check_last_poster (context, forum['id'])
            forum['lastposter'] = lastposter
            forum['lastposttime'] = lastposttime

            # Convert FP result of SUM() above into integer.
            forum['replies'] = int(forum['replies'] or 0)

            # Split moderators list.
            forum['moderators'] = self.unpack_users(forum['moderators'])

            # Split subscriber list to registered and unregistered subscribers.
            forum['subscribers'] = self.unpack_users(forum['subscribers'])
            forum['unregistered_subscribers'] = []
            for subscriber in forum['subscribers']:
                if subscriber not in context.users:
                    forum['unregistered_subscribers'].append(subscriber)

            # Get forum tags.
            self.log.debug(context.resource)

#TagSystem
#            if context.has_tags:
#                tag_system = TagSystem(self.env)
#                forum['tags'] = tag_system.get_tags(context.req, Resource(
#                  'discussion', 'forum/%s' % (forum['id'])))

        return forums

    def get_changed_forums(self, context, start, stop, order_by='time', desc=False):
        """
        Get forums that have changed between start and stop time.

        :param Context context: The Trac request based context
        :param int start: The starting time stamp
        :param int stop: The ending time stamp
        :param str order_by: Which column to order by the results
        :param bool desc: Descending or ascending, True for descending
        :returns: List of dicts describing changed forums
        """
        columns = ('id', 'name', 'author', 'time', 'subject', 'description')
        if not order_by in columns:
            raise TracError("Can't sort by column %s" % order_by)

        sql_values = {'order_by' : ('ORDER BY ' + order_by +
                                    (' ASC', ' DESC')[bool(desc)]) if order_by else ''}
        sql = ("SELECT f.id, f.name, f.author, f.time, f.subject, f.description "
               "FROM forum f "
               "WHERE f.time BETWEEN %%s AND %%s "
               "%(order_by)s " % (sql_values))
        values = (to_timestamp(start), to_timestamp(stop))
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

        # Convert row to dictionaries.
        for row in context.cursor:
            row = dict(zip(columns, row))
            yield row

    def get_topics(self, context, forum_id, order_by='time', desc=False,
                   limit=0, offset=0, with_body=True):
        """
        Get a list of topics, by forum id. Sort by order_by.

        :param Context context: The Trac request based context.
        :param int forum_id: The forum identifier to fetch the topics for
        :param str order_by: Column to order the results by
        :param bool desc: If True, results are sorted in descending order
        :param int limit: Optional argument to limit the amount of results
        :param int offset: Optional argument to start the results from nth element
        :param bool with_body: If True, body of the topic will also be returned
        :returns: List of dicts describing the topics
        """
        def _get_new_replies_count(context, topic_id):
            values = (topic_id, int(context.visited_topics.get(topic_id, 0)))
            sql = '''
                SELECT COUNT(id)
                FROM message m
                WHERE m.topic = %s AND m.time > %s
            '''
            self.log.debug(sql % values)
            context.cursor.execute(sql, values)
            for row in context.cursor:
                return int(row[0])
            return 0

        # make sure only predefined order by's will be used
        if not order_by in ('id', 'time', 'replies', 'lastreply', 'subject', 'viewcount',
                            'author', 'lastreplier', 'status', 'priority'):
            raise TracError("Can't sort messages by %s" % order_by)

        if not order_by in ('replies', 'lastreply'):
            order_by = 't.' + order_by
        if with_body:
            columns = ('id', 'forum', 'time', 'author', 'subscribers',
                       'subject', 'body', 'status', 'priority', 'replies',
                       'lastreply', 'viewcount', 'lastreplier')
        else:
            columns = ('id', 'forum', 'time', 'author', 'subscribers',
                       'subject', 'status', 'priority', 'replies', 'lastreply',
                       'viewcount', 'lastreplier')
        sql_values = {'with_body' : 't.body, ' if with_body else '',
                      'order_by' : ('ORDER BY priority DESC' +
                                    (", " + order_by + (' ASC', ' DESC')[bool(desc)]) if order_by else ''),
                      'limit' : 'LIMIT %s' if limit else '',
                      'offset' : 'OFFSET %s' if offset else ''}
        sql = ("SELECT t.id, t.forum, t.time, t.author, t.subscribers, "
               "t.subject, %(with_body)s t.status, t.priority, m.replies, "
               "IFNULL(m.lastreply,t.time) as lastreply, t.viewcount, lr.lastreplier "
               "FROM topic t "
               "LEFT JOIN (SELECT COUNT(id) AS replies, MAX(time) AS lastreply, topic FROM message GROUP BY topic) m "
               "ON t.id = m.topic "
               "LEFT JOIN (select m.time as time, m.id as id, m.author as lastreplier from message m) lr ON "
               "lastreply = lr.time "
               "WHERE t.forum = %%s "
               "%(order_by)s "
               "%(limit)s "
               "%(offset)s" % (sql_values))
        values = [forum_id]
        if limit:
            values.append(limit)
        if offset:
            values.append(offset)
        values = tuple(values)

        # Execute the query.
        self.env.log.debug(sql % values)
        context.cursor.execute(sql, values)

        # Convert result to dictionaries.
        topics = []
        for row in context.cursor:
            row = dict(zip(columns, row))
            topics.append(row)

        for topic in topics:
            # Compute count of new replies.
            topic['new_replies'] = _get_new_replies_count(context, topic['id'])
            topic['subscribers'] = self.unpack_users(topic['subscribers'])
            topic['unregistered_subscribers'] = []
            for subscriber in topic['subscribers']:
                if subscriber not in context.users:
                    topic['unregistered_subscribers'].append(subscriber)
            topic['status'] = self._topic_status_to_list(topic['status'])
            topic['visualstatus'] = self._topic_status_to_visual_list(topic['status'], topic['priority'])
# TagSystem not used yet
#            if context.has_tags:
#                tag_system = TagSystem(self.env)
#                topic['tags'] = tag_system.get_tags(context.req, Resource(
#                  'discussion', 'topic/%s' % (topic['id'])))

        view_update_sql = "UPDATE forum SET viewcount = IFNULL(viewcount,0) + 1 WHERE id = %s"
        self.env.log.debug(view_update_sql % (forum_id,))
        context.cursor.execute(view_update_sql, (forum_id,))

        return topics

    def get_changed_topics(self, context, start, stop, order_by='time', desc=False):
        """
        Gets topics between two time stamps. This is used to determine if any forums
        have new items in them (the start time comes from user's preferences).

        :param Context context: The Trac request based context.
        :param int start: Starting timestamp to search for changed topics
        :param int stop: Ending timestamp to search for changed topics
        :param str order_by: Column name to order the results by
        :param bool desc: If set to True, the results are sorted in descending order
        :returns: List of dicts describing the topics
        """
        columns = ('id', 'forum', 'forum_name', 'time', 'author', 'subject',
                   'status')
        if order_by not in columns:
            raise TracError("Cannot sort by column %s" % order_by)

        sql_values = {'order_by' : ('ORDER BY ' + order_by +
                                    (' ASC', ' DESC')[bool(desc)]) if order_by else ''}
        sql = ("SELECT t.id, t.forum, f.name, t.time, t.author, t.subject, "
               "t.status "
               "FROM topic t "
               "LEFT JOIN "
               "(SELECT id, name "
               "FROM forum) f "
               "ON t.forum = f.id "
               "WHERE t.time BETWEEN %%s AND %%s "
               "%(order_by)s" % (sql_values))
        values = (to_timestamp(start), to_timestamp(stop))
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)
        for row in context.cursor:
            row = dict(zip(columns, row))
            row['status'] = self._topic_status_to_list(row['status'])
            yield row

    def get_messages(self, context, topic_id, order_by='time', desc=False):
        #make sure only predefined order by's will be used
        columns = ('id', 'replyto', 'time', 'author', 'body')
        if not order_by in columns:
            raise TracError("Can't sort by %s" % order_by)

        order_by = 'm.' + order_by
        sql = ("SELECT m.id, m.replyto, m.time, m.author, m.body FROM message m WHERE"
               " m.topic = %%s ORDER BY %s %s" % (order_by, ("ASC", "DESC")[bool(desc)]))
        self.log.debug(sql % (topic_id,))
        context.cursor.execute(sql, (topic_id,))
        messagemap = {}
        messages = []
        for row in context.cursor:
            row = dict(zip(columns, row))
            messagemap[row['id']] = row

            # Add top-level messages to the main list, in order of time.
            if row['replyto'] == -1:
                messages.append(row)

        # Second pass, add replies.
        for message in messagemap.values():
            if message['replyto'] != -1:
                parent = messagemap[message['replyto']]
                if 'replies' in parent:
                    parent['replies'].append(message)
                else:
                    parent['replies'] = [message]

        # Add post counts for each user
#        for message in messages

        return messages

    def get_users_message_count(self, context):
        """
        This methond fetches all the message counts for all the users. Should be cached.

        returns a dict like {'username1':2, 'username2':4}
        """

        sql_topic = "SELECT author, COUNT(id) FROM topic GROUP BY author"

        self.log.debug(sql_topic)
        context.cursor.execute(sql_topic)
        msg_counts = {}
        for row in context.cursor:
            if row[0] != '':
                msg_counts[row[0]] = row[1]

        sql_message = "SELECT author, COUNT(id) FROM message GROUP BY author"

        self.log.debug(sql_message)
        context.cursor.execute(sql_message)
        for row in context.cursor:
            if row[0] != '':
                if msg_counts.has_key(row[0]):
                    msg_counts[row[0]] = msg_counts[row[0]] + row[1]
                else:
                    msg_counts[row[0]] = row[1]

        return msg_counts

    def get_flat_messages(self, context, topic_id, order_by='time', desc=False,
                          limit=0, offset=0):
        """
        Get messages in flat "layout" for a specific topic. Internally calls _get_items.
        """
        items = self._get_items(
            context, 'message', ('id', 'replyto', 'time', 'author', 'body'),
            where='topic', values=(topic_id,), order_by=order_by, desc=desc, limit=limit,
            offset=offset)

        view_update_sql = "UPDATE topic SET viewcount = IFNULL(viewcount,0) + 1 WHERE id = %s"
        self.log.debug(view_update_sql % topic_id)
        context.cursor.execute(view_update_sql, (topic_id,))

        return items

    def get_flat_messages_by_forum(self, context, forum_id, order_by='time', desc=False,
                                   limit=0, offset=0):
        """
        Return messages of specified forum. Internally uses _get_items
        """
        return self._get_items(
            context, 'message', ('id', 'replyto', 'topic', 'time', 'author', 'body'),
            'forum', (forum_id,), order_by, desc, limit, offset)

    def get_changed_messages(self, context, start, stop, order_by='time', desc=False):
        """
        Get messages between certain time stamps.
        """
        columns = ('id', 'forum', 'forum_name', 'topic', 'topic_subject', 'time', 'author')

        if not order_by in columns:
            raise TracError("Can't sort by column %s" % order_by)

        sql_values = {'order_by' : 'ORDER BY %s %s' % (order_by,
                                                       ('ASC', 'DESC')[bool(desc)]) if order_by else ''}
        sql = ("SELECT m.id, m.forum, f.name, m.topic, t.subject, m.time, "
               "m.author "
               "FROM message m "
               "LEFT JOIN "
               "(SELECT id, name "
               "FROM forum) f "
               "ON m.forum = f.id "
               "LEFT JOIN "
               "(SELECT id, subject "
               "FROM topic) t "
               "ON m.topic = t.id "
               "WHERE time BETWEEN %%s AND %%s "
               "%(order_by)s" % (sql_values))
        values = (to_timestamp(start), to_timestamp(stop))
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)
        for row in context.cursor:
            row = dict(zip(columns, row))
            yield row

    def get_replies(self, context, message_id, order_by='time', desc=False):
        # Return replies of specified message.
        return self._get_items(
            context, 'message', ('id', 'replyto', 'time', 'author', 'body'),
            'replyto', (message_id,), order_by, desc)

    def get_users(self, context):
        # Return users that Trac knows.
        users = []

        # changed to fetch only current project's users
        env_name = conf.resolveProjectName(self.env)
#        env_name = context.req.base_path[1:]
        project_id = resolve_project_id(env_name)
        prjs = Projects()
        prj = prjs.get_project(project_id)
        projectusers = prj.get_team()

        for user in projectusers:
            users.append(user.username)

        return users

    # Add items functions.

    def _add_item(self, context, table, item):
        """
        Adds item into table. The item's values are escaped using cursor's execute
        method. The keys in the item are always hard coded.

        :param Context context: The Trac Context holding request specific data
        :param str table: The table to add into, must be message, forum_group, forum
            or topic.
        :param dict item: Dictionary of column and value pairs.
        :raises: TracError if table name is invalid, database error if query fails.
        """
        if not table in self.VALID_TABLES:
            raise TracError("Cannot add %s" % table)

        # Massive key check loop, have to check all supported column names...
        fields = []
        for key in item.keys():
            if not key in self.VALID_COLUMNS:
                raise TracError("Cannot add a value to %s" % key)
            fields.append(key)

        values = tuple(item.values())
        sql_values = {'table' : table,
                      'fields' : ', '.join(fields),
                      'values' : ','.join(["%s" for I in range(len(values))])} #@UnusedVariable
        sql = '''
            INSERT INTO %(table)s (%(fields)s)
            VALUES (%(values)s)
        ''' % (sql_values)
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

    def add_group(self, context, group):
        self._add_item(context, 'forum_group', group)

    def add_forum(self, context, forum):
        tmp_forum = deepcopy(forum)

        # Pack moderators and subscribers fields.
        tmp_forum['moderators'] = self.pack_users(tmp_forum['moderators'])
        tmp_forum['subscribers'] = self.pack_users(tmp_forum['subscribers'])

        # Remove tags field.
        if tmp_forum.has_key('tags'):
            del tmp_forum['tags']

        # Add forum.
        self._add_item(context, 'forum', tmp_forum)

    def add_topic(self, context, topic):
        tmp_topic = deepcopy(topic)

        # Pack subscribers field.
        tmp_topic['subscribers'] = self.pack_users(tmp_topic['subscribers'])
        tmp_topic['status'] = self._topic_status_from_list(
            tmp_topic['status'] if tmp_topic.has_key('status') else [])

        self._add_item(context, 'topic', tmp_topic)

    def add_message(self, context, message):
        self._add_item(context, 'message', message)

    # Delete items functions.

    def _delete_item(self, context, table, where='', values=()):
        """
        Deletes an item from database. Can be used to delete (empty) whole table
        or just specific item based on a column value.

        :param str table: Table name to delete. Must be one of the following:
            message, topic, forum or forum_group
        :param str where: Column name to limit the deletion by
        :param tuple values: Tuple of single value to use to limit the deletion
            by, used together the column name passed in param where.
        """
        sql = None

        if not table in self.VALID_TABLES:
            raise TracError("Cannot delete from %s" % table)

        if where:
            if where not in self.VALID_COLUMNS:
                raise TracError("Cannot limit deletion by column %s" % where)
            sql_values = {'table': table, 'where': where}
            sql = '''
                DELETE FROM %(table)s
                WHERE %(where)s = %%s
            ''' % sql_values
        else:
            sql = "DELETE FROM %s" % table

        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

    def delete_group(self, context, forum_id):
        """
        Deletes a forum group. Internally, calls _delete_item to do the actual
        deletion.

        :param Context context: The trac request based context.
        :param int forum_id: The forum id to delete
        """
        # Delete group.
        self._delete_item(context, 'forum_group', 'id', (forum_id,))

        # Assing forums of this group to none group.
        self._set_item(context, 'forum', 'forum_group', '0', 'forum_group',
                       (forum_id,))

    def delete_forum(self, context, forum_id):
        # Delete all messages of this forum.
        self._delete_item(context, 'message', 'forum', (forum_id,))

        # Delete all topics of this forum.
        self._delete_item(context, 'topic', 'forum', (forum_id,))

        # Finally delete forum.
        self._delete_item(context, 'forum', 'id', (forum_id,))

    def delete_topic(self, context, topic_id):
        # Delete all messages of this topic.
        self._delete_item(context, 'message', 'topic', (topic_id,))

        # Delete topic itself.
        self._delete_item(context, 'topic', 'id', (topic_id,))

    def delete_message(self, context, message_id):
        # Delete all replies of this message.
        for reply in self.get_replies(context, message_id):
            self.delete_message(context, reply['id'])

        # Delete message itself.
        self._delete_item(context, 'message', 'id', (message_id,))

    # Set item functions.

    def _set_item(self, context, table, column, value, where='', values=()):
        """
        Updates item into database, similarly to _edit_item, but allows selection
        by a custom column. And only allows setting a specific value in a specific
        column.
        """
        if not table in self.VALID_TABLES:
            raise TracError("Cannot update table %s" % table)

        if not column in self.VALID_COLUMNS:
            raise TracError("Cannot update column %s" % column)

        if not where in self.VALID_COLUMNS:
            raise TracError("Cannot select row by column %s value" % where)

        sql_values = {'table' : table,
                      'column' : column,
                      'where' : 'WHERE %s = %%s' % where if where else ''}
        sql = ("UPDATE %(table)s "
               "SET %(column)s = %%s "
               "%(where)s" % (sql_values))
        values = (value,) + values
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

    def set_group(self, context, forum_id, group_id):
        # Change group of specified forum.
        self._set_item(context, 'forum', 'forum_group', group_id or '0',
                       'id', (forum_id,))

    def set_forum(self, context, topic_id, forum_id):
        # Change forum of all topics and messages.
        self._set_item(context, 'topic', 'forum', forum_id, 'id',
                       (topic_id,))
        self._set_item(context, 'message', 'forum', forum_id, 'topic',
                       (topic_id,))

    # Edit functions.

    def _edit_item(self, context, table, item_id, item):
        """
        Edits an item, basically any table in discussion based database.

        :param Context context: The trac request based context
        :param str table: The table name to update, has to be one of the following:
            message, topic, forum, forum_group
        :param str item_id: The identifier for item to be updated
        :param dict item: The actual item content to update into database
        """
        # Allow only specific tables
        if not table in self.VALID_TABLES:
            raise TracError("Cannot edit table %s" % table)

        # Massive key check loop, have to check all supported column names...
        fields = []
        for key in item.keys():
            if not key in self.VALID_COLUMNS:
                raise TracError("Cannot edit a value in %s" % key)
            fields.append(key)

        values = item.values()
        values.append(item_id)
        values = tuple(values)
        sql_values = {'table' : table,
                      'fields' : ", ".join([("%s = %%s" % (field)) for field in fields])}
        sql = '''
            UPDATE %(table)s
            SET %(fields)s
            WHERE id = %%s
        ''' % (sql_values)
        self.log.debug(sql % values)
        context.cursor.execute(sql, values)

    def edit_group(self, context, group_id, group):
        # Edit froum group.
        self._edit_item(context, 'forum_group', group_id, group)

    def edit_forum(self, context, forum_id, forum):
        tmp_forum = deepcopy(forum)

        # Pack moderators and subscribers fields.
        if tmp_forum.has_key('moderators'):
            tmp_forum['moderators'] = self.pack_users(tmp_forum['moderators'])
        if forum.has_key('subscribers'):
            tmp_forum['subscribers'] = self.pack_users(tmp_forum['subscribers'])

        # Edit forum.
        self._edit_item(context, 'forum', forum_id, tmp_forum)

    def edit_topic(self, context, topic_id, topic):
        tmp_topic = deepcopy(topic)

        # Pack subscribers field.
        if tmp_topic.has_key('subscribers'):
            tmp_topic['subscribers'] = self.pack_users(tmp_topic['subscribers'])

        # Encode status field.
        if tmp_topic.has_key('status'):
            tmp_topic['status'] = self._topic_status_from_list(tmp_topic['status'])

        # Edit topic.
        self._edit_item(context, 'topic', topic_id, tmp_topic)

    def edit_message(self, context, message_id, message):
        # Edit message,
        self._edit_item(context, 'message', message_id, message)

    def _get_news_forum_id (self, context):
        row = self._get_item(context, 'forum', ('id',), 'name', (conf.news_forum_name,))
        if row:
            return row['id']

        return None

    # Subscriber, moderator and other user utility functions

    def get_subscribers(self, context, use_unregistered):
        """
        Return a valid subscribers list from given context. If the context's content is
        already a list, return the list as it is. Note that this needs to be the context that
        is given in, not the subs list. Last argument is a bool to decide whether we want to
        include the unregistered subscribers or not.
        """
        if context is None:
            return []

        sub_list = []
        subs = context.req.args.get('subscribers')
        sub_list = self.unpack_users(subs)

        if use_unregistered is True:
            unreg_subs = context.req.args.get('unregistered_subscribers')
            sub_list += self.unpack_users(unreg_subs, sub_list)

        for sub in sub_list:
            if len(sub) > 150:
                raise TracError('Subscriber username or email "%s" is too '
                                'long (more than 150 characters)' % sub)

        return sub_list

    def get_moderators(self, context):
        """
        Return a valid moderator list from given context.
        """
        if context is None:
            return []
        mods = context.req.args.get('moderators')

        return self.unpack_users(mods)

    def unpack_users(self, users, orig=None):
        """
        Take in a user name as a string and unpack it into a list. If users is None,
        empty list is returned. If the the users is a list, it is returned unmodified.
        If any work is done, and orig is present, this code will also check that the
        user name or email is not already in the list.
        """
        unpacked = []

        if users is None:
            return unpacked
        if isinstance(users, list):
            return users
        if orig is None:
            orig = []

        assert isinstance(orig, list)

        # Separator can be a whitespace, comma or semicolon
        names = users.replace(',', ' ').replace(';', ' ')

        # Simple uniqueness check
        for user in names.split():
            if user not in unpacked and user not in orig:
                unpacked.append(user)

        return unpacked

    def pack_users(self, users):
        """
        Pack users into a single string, for storing into database. If users list
        is None, then None is returned. Other wise the list is packed into string
        with whitespace as a separator. The simple join call could be inlined in
        the code everywhere, but this way the separator is defined only once.
        Perhaps for future use, this could be reused as formatting users (moderators
        and subscribers alike) in a different manner.
        """
        if users is None:
            return None

        return ' '.join(users)

# Formats wiki text to signle line HTML but removes all links.
def format_to_oneliner_no_links(env, context, content):
    stream = HTML(format_to_oneliner(env, context, to_unicode(content)))
    return Markup(stream | Transformer('//a').unwrap())
